Structuring Go application in a modular way

Tanveer Prottoy
5 min readSep 16, 2022
Photo by Esther Jiao on Unsplash

In this article we will structure a Go application in a modular fashion. We will only discuss the important parts of the example project.

The structure

The image below shows the structure of the application.

structure

Entry point

Let’s start with the application entry point the main.go file. It is located inside of the cmd directory of the project.

package mainimport "txp/restapistarter/internal/app"func main() {
a := new(app.App)
a.InitComponents()
a.Run()
}

The ‘app’ Package

The ‘app’ package consists of the app.go file and the module directory.

app.go

package appimport (
"log"
"net/http"
"txp/restapistarter/internal/app/module/content"
"txp/restapistarter/internal/app/module/user"
"txp/restapistarter/pkg/data/nosql/mongodb"
"txp/restapistarter/pkg/data/sql/postgres"
"txp/restapistarter/pkg/router"
)
// App struct
type App struct {
DBClient *mongodb.DBClient
router *router.Router
Configs map[string]interface{}
UserModule *user.UserModule
ContentModule *content.ContentModule
}
func (a *App) initDB() {
postgres.InitDBClient()
a.DBClient = mongodb.NewDBClient()
}
func (a *App) initModules() {
a.UserModule = user.NewUserModule(a.DBClient.DB, a.router)
a.ContentModule = content.NewContentModule(a.DBClient.DB, a.router)
}
// Init app
func (a *App) InitComponents() {
a.initDB()
a.router = router.NewRouter()
a.initModules()
}
// Run app
func (a *App) Run() {
err := http.ListenAndServe(
":8080",
a.router.Mux,
)
if err != nil {
log.Fatal(err)
}
}

The ‘module’ Directory

‘module’ directory contains contents and users directories. These are the two modules of the app.

The ‘user’ Package

This package contains the module setup code, handler, service, repository and other components of the user module.

user_module.go

package userimport (
"txp/restapistarter/pkg/router"
"go.mongodb.org/mongo-driver/mongo"
)
type UserModule struct {
Router *UserRouter
Handler *UserHandler
Service *UserService
Repository *UserRepository
}
func NewUserModule(db *mongo.Database, router *router.Router) *UserModule {
m := new(UserModule)
// init order is reversed of the field decleration
// as the dependency is served this way
m.Repository = new(UserRepository)
m.Service = NewUserService(m.Repository)
m.Handler = NewUserHandler(m.Service)
m.Router = NewUserRouter(router, m)
return m
}

user_handler.go

package userimport (
"net/http"
)
type UserHandler struct {
service *UserService
}
func NewUserHandler(s *UserService) *UserHandler {
h := new(UserHandler)
h.service = s
return h
}
func (h *UserHandler) Create(w http.ResponseWriter, r *http.Request) {
h.service.Create(w, r)
}
func (h *UserHandler) ReadMany(w http.ResponseWriter, r *http.Request) {
h.service.ReadMany(w, r)
}
func (h *UserHandler) ReadOne(w http.ResponseWriter, r *http.Request) {
h.service.ReadOne(w, r)
}
func (h *UserHandler) Update(w http.ResponseWriter, r *http.Request) {
h.service.Update(w, r)
}
func (h *UserHandler) Delete(w http.ResponseWriter, r *http.Request) {
h.service.Delete(w, r)
}

user_service.go

package userimport (
"log"
"net/http"
"txp/restapistarter/internal/app/module/user/dto"
"txp/restapistarter/internal/app/module/user/schema"
"txp/restapistarter/internal/pkg/constant"
"txp/restapistarter/pkg/data/nosql/mongodb"
"txp/restapistarter/pkg/json"
"txp/restapistarter/pkg/response"
"txp/restapistarter/pkg/router"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo"
)
type UserService struct {
repository *UserRepository
}
func NewUserService(r *UserRepository) *UserService {
s := new(UserService)
s.repository = r
return s
}
func (s *UserService) Create(w http.ResponseWriter, r *http.Request) {
var b dto.CreateUpdateUserDto
err := json.Decode(r.Body, &b)
if err != nil {
response.RespondError(http.StatusBadRequest, err, w)
return
}
res, err := s.repository.Create(
constant.UsersCollection,
r.Context(),
&schema.User{
Name: b.Name,
},
nil,
)
if err != nil {
response.RespondError(http.StatusInternalServerError, err, w)
return
}
log.Println(res)
response.Respond(http.StatusOK, response.BuildData(res), w)
}
func (s *UserService) ReadMany(w http.ResponseWriter, r *http.Request) {
c, err := s.repository.ReadMany(
constant.UsersCollection,
r.Context(),
bson.D{},
nil,
)
if err != nil {
response.RespondError(http.StatusInternalServerError, err, w)
return
}
var data []schema.User
data, err = mongodb.DecodeCursor[[]schema.User](c, r.Context())
if err != nil {
if err == mongo.ErrNoDocuments {
// This error means your query did not match any documents.
response.Respond(http.StatusOK, make([]any, 0), w)
return
} else if err == mongo.ErrNilCursor {
// This error means your query did not match any documents.
response.Respond(http.StatusOK, make([]any, 0), w)
return
}
response.RespondError(http.StatusInternalServerError, err, w)
return
}
response.Respond(http.StatusOK, response.BuildData(data), w)
}
func (s *UserService) ReadOne(w http.ResponseWriter, r *http.Request) {
id := router.GetURLParam(r, constant.UrlKeyId)
objId, err := mongodb.BuildObjectID(id)
if err != nil {
response.RespondError(http.StatusBadRequest, err, w)
return
}
filter := bson.D{{Key: "_id", Value: bson.D{{Key: "$eq", Value: objId}}}}
res := s.repository.ReadOne(
constant.UsersCollection,
r.Context(),
filter,
nil,
)
var data schema.User
data, err = mongodb.DecodeSingleResult[schema.User](res)
if err != nil {
/* if err == mongo.ErrNoDocuments {
This error means your query did not match any documents.
} */
response.RespondError(http.StatusNotFound, err, w)
return
}
response.Respond(http.StatusOK, response.BuildData(data), w)
}
func (s *UserService) Update(w http.ResponseWriter, r *http.Request) {
id := router.GetURLParam(r, constant.UrlKeyId)
objId, err := mongodb.BuildObjectID(id)
if err != nil {
response.RespondError(http.StatusBadRequest, err, w)
return
}
var b dto.CreateUpdateUserDto
err = json.Decode(r.Body, b)
if err != nil {
response.RespondError(http.StatusBadRequest, err, w)
return
}
filter := bson.D{{Key: "_id", Value: bson.D{{Key: "$eq", Value: objId}}}}
doc := bson.D{{Key: "$set", Value: bson.D{{Key: "name", Value: b.Name}}}}
res, err := s.repository.Update(
constant.UsersCollection,
r.Context(),
filter,
doc,
nil,
)
if err != nil {
response.RespondError(http.StatusInternalServerError, err, w)
return
}
log.Println(res)
response.Respond(http.StatusOK, response.BuildData(res), w)
}
func (s *UserService) Delete(w http.ResponseWriter, r *http.Request) {
id := router.GetURLParam(r, constant.UrlKeyId)
objId, err := mongodb.BuildObjectID(id)
if err != nil {
response.RespondError(http.StatusBadRequest, err, w)
return
}
filter := bson.D{{Key: "_id", Value: bson.D{{Key: "$eq", Value: objId}}}}
res, err := s.repository.Delete(
constant.UsersCollection,
r.Context(),
filter,
nil,
)
if err != nil {
response.RespondError(http.StatusInternalServerError, err, w)
return
}
response.Respond(http.StatusOK, response.BuildData(res), w)
}

user_repository.go

package userimport (
"context"
"txp/restapistarter/pkg/data/nosql/mongodb"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
)
type UserRepository struct {
DB *mongo.Database
}
func NewUserRepository(db *mongo.Database) *UserRepository {
r := new(UserRepository)
r.DB = db
return r
}
func (r *UserRepository) Create(
collectionName string,
ctx context.Context,
doc any,
opts ...*options.InsertOneOptions,
) (*mongo.InsertOneResult, error) {
return mongodb.InsertOne(
r.DB,
collectionName,
ctx,
doc,
opts...,
)
}
func (r *UserRepository) ReadMany(
collectionName string,
ctx context.Context,
filter any,
opts ...*options.FindOptions,
) (*mongo.Cursor, error) {
return mongodb.Find(
r.DB,
collectionName,
ctx,
filter,
opts...,
)
}
func (r *UserRepository) ReadOne(
collectionName string,
ctx context.Context,
filter any,
opts ...*options.FindOneOptions,
) *mongo.SingleResult {
return mongodb.FindOne(
r.DB,
collectionName,
ctx,
filter,
opts...,
)
}
func (r *UserRepository) Update(
collectionName string,
ctx context.Context,
filter any,
doc any,
opts ...*options.UpdateOptions,
) (*mongo.UpdateResult, error) {
return mongodb.UpdateOne(
r.DB,
collectionName,
ctx,
filter,
doc,
opts...,
)
}
func (r *UserRepository) Delete(
collectionName string,
ctx context.Context,
filter any,
opts ...*options.DeleteOptions,
) (*mongo.DeleteResult, error) {
return mongodb.DeleteOne(
r.DB,
collectionName,
ctx,
filter,
opts...,
)
}

The source code is available here: https://github.com/tanveerprottoy/starter-go

That’s it for this article. Please don’t forget to add some claps.

--

--